gem 'acts_as_taggable', '>= 2.0.2'
gem 'coderay', '>= 0.7.4'
gem 'RedCloth'

require 'taggable'
require 'coderay'
require 'redcloth'
require 'ostruct'

class Post < ActiveRecord::Base
  belongs_to :user, :foreign_key => 'author_id'
  belongs_to :blog
  acts_as_taggable :join_class_name => 'TagPost'
  
  def body
    Body.new(super)
  end
  
  def validate
    begin
      body.to_xhtml
    rescue InvalidMarkup
      errors.add(:body, "is invalid. #{$!}")
    end
  end
  
  class Body
    OPEN_CODE = /(\[code:?(.*?)\])/
    CLOSE_CODE = '[/code]'
    
    attr_accessor :text
    
    def initialize(text)
      @text = text
    end
    
    def to_xhtml
      self.class.split_into_blocks(text).collect{|b| b.to_xhtml}.join
    end
    
    class Block < OpenStruct
      def to_xhtml
        case kind
        when :textile then return RedCloth.new(text).to_html
        when :code then return CodeRay.scan(text, language).html(:css => :class).div
        else raise "#{type} is not a valid #{self.class} type." end
      end
    end
    
    private
      # Recursively splits and returns the text as an array of Blocks.
      # Currently there are only two types of blocks :code, and :textile.
      #
      # For example, the following text:
      #
      #   This is some *textile* markup.
      #   and some more... And below is ruby code:
      #
      #   [code:ruby]
      #     def example
      #       return true
      #     end
      #   [/code]
      #   
      #   And _some more_ textile.
      #
      # ... would be returned as the following array:
      #
      #   [#<Post::Body::Block kind=:textile, text="This is some *textile* markup.\nand some more... And below is ruby code:\n\n">,
      #     #<Post::Body::Block kind=:code, text="\ndef example\n  return true\nend\n", language=:ruby>,
      #     #<Post::Body::Block kind=:textile, text="\n   \nAnd _some more_ textile.\n">]
      # 
      # Note the 'kind' and 'language' attributes on each of those Post::Body::Block objects.
      def self.split_into_blocks(text)
        block = Block.new
        open_code = text.index(OPEN_CODE)
        
        if open_code.nil?
          block.kind = :textile
          block.text = text
        elsif open_code == 0
          close_code = text.index(CLOSE_CODE)
          raise InvalidMarkup, "You are missing a #{CLOSE_CODE} tag to close the code block." unless close_code
          block.kind = :code
          block.language = $2.to_sym
          block.text = text[$1.length..close_code-1]
          remainder = text[close_code+CLOSE_CODE.length..-1]
        else
          block.kind = :textile
          block.text = text[0..open_code-1]
          remainder = text[open_code..-1]
        end
        
        if remainder
          return [block] + split_into_blocks(remainder)
        else
          return [block]
        end
      end
  end
  
  class InvalidMarkup < Exception
  end
end